<!doctype html>
<script src="../resources/testharness.js"></script>
<script src="../resources/testharnessreport.js"></script>
<script src="assert_selection.js"></script>
<script>
function checked_assert_selection(input, command, output) {
    try {
        assert_selection(input, command, output);
    } catch (exception) {
        return exception.message;
    }
    return 'no exception';
}

test(() => {
    assert_selection('foo', 'noop', 'foo');
    assert_selection('|foo', 'noop', '|foo');
    assert_selection('f|oo', 'noop', 'f|oo');
    assert_selection('foo|', 'noop', 'foo|');
    assert_selection('^foo|', 'noop', '^foo|');
    assert_selection('f^oo|', 'noop', 'f^oo|');
    assert_selection('f^o|o', 'noop', 'f^o|o');
    assert_selection('|foo^', 'noop', '|foo^');
    assert_selection(
        '|foo^',
        selection => selection.modify('extend', 'forward', 'character'),
        'f|oo^', 'selection.modify');
    assert_selection(
        '<div contenteditable>^foo|</div>',
        'bold',
        '<div contenteditable><b>^foo|</b></div>',
        'execCommand');
}, 'markers in text');

test(() => {
    assert_selection('|<img>', 'noop', '|<img>');
    assert_selection('^<img>|', 'noop', '^<img>|');
    assert_selection('|<img>^', 'noop', '|<img>^');
    assert_selection('<a>|<br></a>', 'noop', '<a>|<br></a>');
    assert_selection('<a><img>|</a>', 'noop', '<a><img>|</a>');
    assert_selection('<a>^<img>|</a>', 'noop', '<a>^<img>|</a>');
    assert_selection('<a>|<img>^</a>', 'noop', '<a>|<img>^</a>');
    assert_selection('<a><img>|<img></a>', 'noop', '<a><img>|<img></a>');
    assert_selection('<a><img>|<img>^</a>', 'noop', '<a><img>|<img>^</a>');
    assert_selection('<a><img>^<img>|</a>', 'noop', '<a><img>^<img>|</a>');
}, 'markers at element');

test(() => {
    assert_selection(
        '|<table><tr><td>foo</td></tr></table>', 'noop',
        '|<table><tbody><tr><td>foo</td></tr></tbody></table>');
    assert_selection(
        '<table><tr><td>foo</td></tr></table>|', 'noop',
        '<table><tbody><tr><td>foo</td></tr></tbody></table>|');
    assert_selection(
        '^<table><tr><td>foo</td></tr></table>|', 'noop',
        '^<table><tbody><tr><td>foo</td></tr></tbody></table>|');
    assert_selection(
        '|<table><tr><td>foo</td></tr></table>^', 'noop',
        '|<table><tbody><tr><td>foo</td></tr></tbody></table>^');
}, 'markers around table');

selection_test(
    '<div>foo</div>',
    selection => {
        let doc = selection.document;
        doc.documentElement.replaceChild(
            doc.createTextNode('baz'), doc.body);
    },
    '<html><head></head>baz</html>',
    'Serialize document element instead of document.body when it is null.');

// Tests for TEXTAREA
selection_test(
    '<div>|<textarea>foo</textarea></div>',
    'noop',
    '<div>|<textarea>foo</textarea></div>',
    'We can place caret before TEXTAREA');

selection_test(
    '<div><textarea>foo</textarea>|</div>',
    'noop',
    '<div><textarea>foo</textarea>|</div>',
    'We can place caret after TEXTAREA');

selection_test(
    '<div>^<textarea>foo</textarea>|</div>',
    'noop',
    '<div>^<textarea>foo</textarea>|</div>',
    'We can select TEXTAREA');

selection_test(
    '<div><textarea>f|oo</textarea></div>',
    selection => {
      const textarea = selection.document.querySelector('textarea');
      textarea.value = 'bar';
    },
    '<div><textarea>bar|</textarea></div>',
    'TEXTAREA is serialized with value property');

selection_test(
    '<div><textarea>01234|56789</textarea></div>',
    selection => {
      const textarea = selection.document.querySelector('textarea');
      textarea.setSelectionRange(3, 7);
    },
    '<div><textarea>012^3456|789</textarea></div>',
    'HTMLTextArea#setSelectionRange()');

selection_test(
    '<textarea>0123^456|789</textarea>',
    selection => {
      const textarea = selection.document.querySelector('textarea');
      assert_equals(textarea.selectionStart, 4, 'selectionStart');
      assert_equals(textarea.selectionEnd, 7, 'selectionEnd');
    },
    '<textarea>0123^456|789</textarea>',
    'range selection');

selection_test(
    '<textarea>0123|456^789</textarea>',
    selection => {
      const textarea = selection.document.querySelector('textarea');
      assert_equals(textarea.selectionStart, 4, 'selectionStart');
      assert_equals(textarea.selectionEnd, 7, 'selectionEnd');
      assert_equals(textarea.selectionDirection, 'backward', 'selectionEnd');
    },
    '<textarea>0123|456^789</textarea>',
    'backward range selection');

selection_test(
    '<textarea>01234|56789</textarea>',
    selection => {
      const textarea = selection.document.querySelector('textarea');
      assert_equals(textarea.selectionStart, 5, 'selectionStart');
      assert_equals(textarea.selectionEnd, 5, 'selectionEnd');
    },
    '<textarea>01234|56789</textarea>',
    'caret selection');

// Shadow DOM
selection_test(
    [
        '<div id="host">',
            '<b id="abc" slot="abc">abc</b>',
            '<b id="def" slot="def">def</b>',
        '</div>',
    ].join(''),
    selection => {
      const document = selection.document;
      const host = document.getElementById('host');
      const shadowRoot = host.attachShadow({mode: 'open'});
      shadowRoot.innerHTML = [
        '<span id="ghi">ghi</span>',
        '<slot name="def"></slot>',
        '<span id="jkl">jkl</span>',
        '<slot name="abc"></slot>',
        '<span id="mno">mno</span>',
      ].join('');
      selection.collapse(document.getElementById('abc'), 0);
      selection.extend(document.getElementById('def'), 0);
    },
    [
        '<div id="host">',
            '<span id="ghi">ghi</span>',
            '<slot name="def">',
                '<b id="def" slot="def">|def</b>',
            '</slot>',
            '<span id="jkl">jkl</span>',
            '<slot name="abc">',
                '<b id="abc" slot="abc">^abc</b>',
            '</slot>',
            '<span id="mno">mno</span>',
        '</div>',
    ].join(''),
    {dumpAs: 'flattree'}, 'dump flat tree for shadow DOM V1');

selection_test(
    [
        '<style>* { font-family: monospace; }</style>',
        '<div>',
          '<span id="start">ABC</span>',
          '<p><a slot="a">111</a><b slot="b">222</b></p>',
          'DEF',
        '</div>',
    ],
    selection => {
      if (!window.eventSender)
        throw 'This test requires eventSender.';
      const document = selection.document;
      const host = document.querySelector('p');
      const shadowRoot = host.attachShadow({mode: 'open'});
      shadowRoot.innerHTML = [
        'ghi',
        '<slot name="b"></slot>',
        '<span id="end">jkl</span>',
        '<slot name="a"></slot>',
        'mno',
      ].join('');
      const start = document.getElementById('start');
      const end = shadowRoot.getElementById('end');
      eventSender.mouseMoveTo(
        selection.computeLeft(start),
        selection.computeTop(start) + start.offsetHeight / 2);
      eventSender.mouseDown();
      eventSender.leapForward(300);
      eventSender.mouseMoveTo(
        selection.computeLeft(end) + end.offsetWidth - 1,
        selection.computeTop(end) + end.offsetHeight / 2);
      eventSender.mouseUp();
    },
    [
        '<style>* { font-family: monospace; }</style>',
        '<div>',
          '<span id="start">^ABC</span>',
          '<p>',
            'ghi',
            '<slot name="b"><b slot="b">222</b></slot>',
            '<span id="end">jkl|</span>',
            '<slot name="a"><a slot="a">111</a></slot>',
            'mno',
          '</p>',
          'DEF',
        '</div>',
    ],
    {dumpAs: 'flattree'}, 'selection crossing shadow boundary');

selection_test(
    '<br>',
    selection => {
        selection.document.documentElement.prepend('Pre-HEAD');
        selection.document.body.before('Pre-BODY');
        selection.document.documentElement.append('Post-BODY');
    },
    '<html>Pre-HEAD<head></head>Pre-BODY<body><br></body>Post-BODY</html>',
    {dumpFromRoot: true},
    'Serialize with dumpFromRoot option');

test(() => {
    assert_equals(checked_assert_selection('fo|o', 'noop', 'fo|o'),
        'no exception');
}, 'no marker in output');

test(() => {
    assert_equals(checked_assert_selection('fo|o|', 'noop', 'foo'),
        'You should have at least one focus marker "|" in "fo|o|".');
}, 'multiple focus markers in input');

test(() => {
    assert_equals(checked_assert_selection('fo|o', 'noop', '|fo|o'),
        'You should have at most one focus marker "|" in "|fo|o".');
}, 'multiple focus markers in output');

test(() => {
    assert_equals(checked_assert_selection('^fo|o^', 'noop', 'foo'),
        'You should have at most one anchor marker "^" in "^fo|o^".');
}, 'multiple anchor markers in input');

test(() => {
    assert_equals(checked_assert_selection('fo|o', 'noop', '^fo|o^'),
        'You should have at most one anchor marker "^" in "^fo|o^".');
}, 'multiple anchor markers in output');

test(() => {
    assert_equals(checked_assert_selection('^foo', 'noop', 'baz'),
        'You should specify caret position in "^foo".');
}, 'anchor marker only in input');

test(() => {
    assert_equals(checked_assert_selection('|foo', 'noop', 'b^az'),
        'You should have a focus marker "|" in "b^az".');
}, 'anchor marker only in output');

test(() => {
    assert_equals(checked_assert_selection('^|foo', 'noop', 'baz'),
        'You should have focus marker and should not have anchor marker if and only if selection is a caret in "^|foo".');
}, 'anchor == focus in input');

test(() => {
    assert_equals(checked_assert_selection('|foo', 'noop', 'b^|az'),
        'You should have focus marker and should not have anchor marker if and only if selection is a caret in "b^|az".');
}, 'anchor == focus in output');

// TODO: It's better to have more powerful diff like
// |CreateUnifiedDiff()| in gtest or "Longest common substring"
test(() => {
    assert_equals(checked_assert_selection('foo', 'noop', 'foz'),
        `editing/assert_selection.html:8:9)\n` +
        `\t expected foz,\n` +
        `\t but got  foo,\n` +
        `\t sameupto fo`);
}, 'Compare result');

selection_test(
    '<div contenteditable><p>^test|</p></div>',
    'insertHTML <span style="color: green">green</span>',
    '<div contenteditable><p><span style="color: green">green|</span></p></div>',
    'multiple spaces in function');

test(() => {
    assert_selection(
        '<div contenteditable>|</div>',
        selection => {
            selection.setClipboardData('<b>foo</b>');
            selection.document.execCommand('paste');
        },
        '<div contenteditable><b>foo|</b></div>',
        'set HTML fragment to clipboard and paste');

    assert_selection(
        '<div contenteditable>|</div>',
        selection => {
            selection.setClipboardData('<b>foo</b>');
            selection.document.execCommand('pasteAndMatchStyle');
        },
        '<div contenteditable>foo|</div>',
        'set HTML fragment to clipboard and pasteAndMatchStyle');

    assert_selection(
        '<div contenteditable>|</div>',
        selection => {
            selection.setClipboardData('<b>foo</b>', 'FOO');
            selection.document.execCommand('pasteAndMatchStyle');
        },
        '<div contenteditable>FOO|</div>',
        'set HTML fragment and text to clipboard and pasteAndMatchStyle');
}, 'selection.setClipboardData');

test(() => {
    const sample1 = assert_selection_and_return_sample('abc', '', 'abc',
        {removeSampleIfSucceeded: false});
    assert_equals(sample1.iframe_.parentNode, document.body,
        'removeSampleIfSucceeded: false');

    const sample2 = assert_selection_and_return_sample('abc', '', 'abc',
        {removeSampleIfSucceeded: true});
    assert_equals(sample2.iframe_.id, Sample.playgroundId,
        'removeSampleIfSucceeded: true: id');
    assert_equals(sample2.iframe_.style.display, 'none',
        'removeSampleIfSucceeded: true: style.display');

    const sample3 = assert_selection_and_return_sample('abc', '', 'abc');
    assert_equals(sample3.iframe_.id, Sample.playgroundId,
        'with default options: id');
    assert_equals(sample3.iframe_.style.display, 'none',
        'with default options: id');

    const sample4 = assert_selection_and_return_sample('abc', '', 'abc', 'description');
    assert_equals(sample4.iframe_.id, Sample.playgroundId,
        'with description: id');
    assert_equals(sample4.iframe_.style.display, 'none',
        'with description: id');
}, 'removeSampleIfSucceeded');

test(() => {
    assert_own_property(window, 'internals');
    assert_own_property(window, 'eventSender');
    assert_selection(
        'foo|bar',
        () => { assert_equals(internals.textAffinity, 'Downstream'); },
        'foo|bar');
    assert_selection(
        '<div contenteditable style="width: 25px;">foobar</div>',
        selection => {
            eventSender.dragMode = false;
            var document =  selection.document;
            var div = document.querySelector('div');
            eventSender.mouseMoveTo(document.offsetLeft + div.offsetLeft + 20,
                document.offsetTop + div.offsetTop + 5);
            eventSender.mouseDown();
            eventSender.mouseUp();
            assert_equals(internals.textAffinity, 'Upstream'); },
        '<div contenteditable style="width: 25px;">foo|bar</div>');
}, 'Textaffinity');

test(() => {
  assert_own_property(window, 'eventSender');
  assert_selection(
    [
      '<div id="first">one <span id="start"></span>two three</div>',
      '<div id="second">four <span id="end"></span>five six</div>',
    ].join(''),
    selection => {
      const start = selection.document.getElementById('start');
      const end = selection.document.getElementById('end');
      eventSender.mouseMoveTo(selection.computeLeft(start),
                              selection.computeTop(start));
      eventSender.mouseDown();
      eventSender.mouseMoveTo(selection.computeLeft(end),
                              selection.computeTop(end));
      eventSender.mouseUp();
    },
    [
      '<div id="first">one <span id="start"></span>^two three</div>',
      '<div id="second">four |<span id="end"></span>five six</div>',
    ].join(''));
}, 'computeLeft() and computeTop()');

// When |selection_test()| revives AsyncFunction as tester, |selection_test()|
// uses |promise_test()| API instead of |test()| API.
selection_test('abc', async () => 1, 'abc', 'use promise_test()');
</script>
